#include "mycoroutine_coroutine.h"
#include "coroutine.h"
#include "base.h"

#define PHP_CORO_TASK_SLOT ((int)((ZEND_MM_ALIGNED_SIZE(sizeof(php_coro_task)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval)) - 1) / ZEND_MM_ALIGNED_SIZE(sizeof(zval))))

long PHPCoroutine_create(zend_fcall_info_cache *fci_cache, uint32_t argc, zval *argv){
	php_coro_args cargs;
	cargs.fcc = fci_cache;
	cargs.argc = argc;
	cargs.argv = argv;

	cg_init();

	PHPCoroutine_save_task(PHPCoroutine_get_task());

	return coroutine_create(PHPCoroutine_create_func,(void *)(&cargs));
}

void PHPCoroutine_save_task(php_coro_task *task){
	PHPCoroutine_save_vm_stack(task);
}

void PHPCoroutine_save_vm_stack(php_coro_task *task){
	task->vm_stack_top = EG(vm_stack_top);
    task->vm_stack_end = EG(vm_stack_end);
    task->vm_stack = EG(vm_stack);
    //task->vm_stack_page_size = EG(vm_stack_page_size);
    task->execute_data = EG(current_execute_data);
}

php_coro_task* PHPCoroutine_get_task(){
	php_coro_task *task = (php_coro_task *) coroutine_get_current_task();
	return task ? task : &main_task;
}

void PHPCoroutine_create_func(void *arg){
	int i;
    php_coro_args *php_arg = (php_coro_args *) arg;
    zend_fcall_info_cache fci_cache = *php_arg->fcc;
    zend_function *func = fci_cache.function_handler;
    zval *argv = php_arg->argv;
    int argc = php_arg->argc;
    php_coro_task *task;
    zend_execute_data *call;
    zval _retval, *retval = &_retval;

    vm_stack_init(); // get a new php stack
    call = (zend_execute_data *) (EG(vm_stack_top));
    task = (php_coro_task *) EG(vm_stack_top);
    EG(vm_stack_top) = (zval *) ((char *) call + PHP_CORO_TASK_SLOT * sizeof(zval));

    call = zend_vm_stack_push_call_frame(
        ZEND_CALL_TOP_FUNCTION | ZEND_CALL_ALLOCATED,
        func, argc, fci_cache.called_scope, fci_cache.object
    );

    for (i = 0; i < argc; ++i)
    {
        zval *param;
        zval *arg = &argv[i];
        param = ZEND_CALL_ARG(call, i + 1);
        ZVAL_COPY(param, arg);
    }

    call->symbol_table = NULL;

    EG(current_execute_data) = call;

    PHPCoroutine_save_vm_stack(task);

    task->co = get_current();
    set_task(task->co,(void *) task);

    if (func->type == ZEND_USER_FUNCTION)
    {
        ZVAL_UNDEF(retval);
        EG(current_execute_data) = NULL;
		zend_init_execute_data(call, &func->op_array, retval);
        zend_execute_ex(EG(current_execute_data));
    }

    zval_ptr_dtor(retval);
}

void vm_stack_init(){
	uint32_t size = DEFAULT_PHP_STACK_PAGE_SIZE;
    zend_vm_stack page = (zend_vm_stack) emalloc(size);

    page->top = ZEND_VM_STACK_ELEMENTS(page);
    page->end = (zval*) ((char*) page + size);
    page->prev = NULL;

    EG(vm_stack) = page;
    EG(vm_stack)->top++;
    EG(vm_stack_top) = EG(vm_stack)->top;
    EG(vm_stack_end) = EG(vm_stack)->end;
    //EG(vm_stack_page_size) = size;
}

void PHPCoroutine_on_close(void *arg){
	php_coro_task *task = (php_coro_task *) arg;
    php_coro_task *origin_task = get_origin_task(task);

	PHPCoroutine_vm_stack_destroy();
    PHPCoroutine_restore_task(origin_task);
}

void PHPCoroutine_restore_task(php_coro_task *task){
	PHPCoroutine_restore_vm_stack(task);
}

void PHPCoroutine_restore_vm_stack(php_coro_task *task){
	EG(vm_stack_top) = task->vm_stack_top;
	EG(vm_stack_end) = task->vm_stack_end;
	EG(vm_stack) = task->vm_stack;
	EG(current_execute_data) = task->execute_data;
}

void PHPCoroutine_vm_stack_destroy(){
	zend_vm_stack stack = EG(vm_stack);

    while (stack != NULL)
    {
        zend_vm_stack p = stack->prev;
        efree(stack);
        stack = p;
    }
}

php_coro_task* get_origin_task(php_coro_task *task){
	Coroutine *co = coroutine_get_origin(task->co);
	return co ? (php_coro_task *) coroutine_get_task(co) : &main_task;
}

void PHPCoroutine_on_yield(void *arg){
	php_coro_task *task = (php_coro_task *) arg;
    php_coro_task *origin_task = get_origin_task(task);
    PHPCoroutine_save_task(task);
    PHPCoroutine_restore_task(origin_task);
}

void PHPCoroutine_on_resume(void *arg){
	php_coro_task *task = (php_coro_task *) arg;
    php_coro_task *current_task = PHPCoroutine_get_task();
    PHPCoroutine_save_task(current_task);
    PHPCoroutine_restore_task(task);
}
